/*
 * Copyright 2008-2010 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package im.composer.media.sound.synth;

import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.stream.Stream;

import javax.sound.midi.Instrument;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Patch;
import javax.sound.midi.Receiver;
import javax.sound.midi.Soundbank;
import javax.sound.midi.Transmitter;
import javax.sound.midi.VoiceStatus;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.SourceDataLine;

import org.tritonus.share.sampled.FloatSampleInput;

/**
 * The software synthesizer class.
 *
 * @author Karl Helgason
 */
public class SoftSynthesizer implements AudioSynthesizer {

	private static class Info extends MidiDevice.Info {
		public Info() {
			super(INFO_NAME, INFO_VENDOR, INFO_DESCRIPTION, INFO_VERSION);
		}
	}

	protected static final String INFO_NAME = "Gervill";
	protected static final String INFO_VENDOR = "OpenJDK";
	protected static final String INFO_DESCRIPTION = "Software MIDI Synthesizer";
	protected static final String INFO_VERSION = "1.0";
	protected final static MidiDevice.Info info = new Info();

	protected static Soundbank defaultSoundBank = null;

	protected Object control_mutex = this;

	protected int voiceIDCounter = 0;

	// 0: default
	// 1: DLS Voice Allocation
	protected int voice_allocation_mode = 0;

	protected boolean load_default_soundbank = false;
	protected boolean reverb_light = true;
	protected boolean reverb_on = true;
	protected boolean chorus_on = true;
	protected boolean agc_on = true;

	protected SoftChannel[] channels;
	protected SoftChannelProxy[] external_channels = null;

	private boolean largemode = false;

	// 0: GM Mode off (default)
	// 1: GM Level 1
	// 2: GM Level 2
	private int gmmode = 0;

	private int deviceid = 0;

	private AudioFormat format = new AudioFormat(44100, 16, 2, true, false);

	private float controlrate = 147f;

	private boolean open = false;
	private boolean implicitOpen = false;

	private String resamplerType = "linear";
	private SoftResampler resampler = new SoftLinearResampler();

	private int number_of_midi_channels = 16;
	private int maxpoly = 64;
	private long latency = 200000; // 200 msec
	private boolean jitter_correction = false;

	private SoftMainMixer mainmixer;
	private SoftVoice[] voices;

	private Map<String, SoftTuning> tunings = new HashMap<String, SoftTuning>();
	private Map<String, SoftInstrument> inslist = new HashMap<String, SoftInstrument>();
	private Map<String, ModelInstrument> loadedlist = new HashMap<String, ModelInstrument>();

	private ArrayList<Receiver> recvslist = new ArrayList<Receiver>();

	private void getBuffers(ModelInstrument instrument, List<ModelByteBuffer> buffers) {
		Stream.of(instrument.getPerformers()).parallel().filter(performer -> (performer.getOscillators() != null)).flatMap(performer -> performer.getOscillators().parallelStream())
				.filter(osc -> osc instanceof ModelByteBufferWavetable).map(osc -> (ModelByteBufferWavetable) osc).map(w -> w.getBuffer()).filter(buf -> buf != null).forEach(buf -> buffers.add(buf));

	}

	private boolean loadSamples(List<ModelInstrument> instruments) {
		if (largemode)
			return true;
		List<ModelByteBuffer> buffers = new ArrayList<ModelByteBuffer>();
		instruments.stream().forEach(instrument->getBuffers(instrument, buffers));
		try {
			ModelByteBuffer.loadAll(buffers);
		} catch (IOException e) {
			return false;
		}
		return true;
	}

	private boolean loadInstruments(List<ModelInstrument> instruments) {
		if (!isOpen())
			return false;
		if (!loadSamples(instruments))
			return false;

		synchronized (control_mutex) {
			if (channels != null)
				for (SoftChannel c : channels) {
					c.current_instrument = null;
					c.current_director = null;
				}
			for (Instrument instrument : instruments) {
				String pat = patchToString(instrument.getPatch());
				SoftInstrument softins = new SoftInstrument((ModelInstrument) instrument);
				inslist.put(pat, softins);
				loadedlist.put(pat, (ModelInstrument) instrument);
			}
		}

		return true;
	}

	private void processPropertyInfo(Map<String, Object> info) {
		AudioSynthesizerPropertyInfo[] items = getPropertyInfo(info);

		String resamplerType = (String) items[0].value;
		if (resamplerType.equalsIgnoreCase("point")) {
			this.resampler = new SoftPointResampler();
			this.resamplerType = "point";
		} else if (resamplerType.equalsIgnoreCase("linear")) {
			this.resampler = new SoftLinearResampler2();
			this.resamplerType = "linear";
		} else if (resamplerType.equalsIgnoreCase("linear1")) {
			this.resampler = new SoftLinearResampler();
			this.resamplerType = "linear1";
		} else if (resamplerType.equalsIgnoreCase("linear2")) {
			this.resampler = new SoftLinearResampler2();
			this.resamplerType = "linear2";
		} else if (resamplerType.equalsIgnoreCase("cubic")) {
			this.resampler = new SoftCubicResampler();
			this.resamplerType = "cubic";
		} else if (resamplerType.equalsIgnoreCase("lanczos")) {
			this.resampler = new SoftLanczosResampler();
			this.resamplerType = "lanczos";
		} else if (resamplerType.equalsIgnoreCase("sinc")) {
			this.resampler = new SoftSincResampler();
			this.resamplerType = "sinc";
		}

		setFormat((AudioFormat) items[2].value);
		controlrate = (Float) items[1].value;
		latency = (Long) items[3].value;
		deviceid = (Integer) items[4].value;
		maxpoly = (Integer) items[5].value;
		reverb_on = (Boolean) items[6].value;
		chorus_on = (Boolean) items[7].value;
		agc_on = (Boolean) items[8].value;
		largemode = (Boolean) items[9].value;
		number_of_midi_channels = (Integer) items[10].value;
		jitter_correction = (Boolean) items[11].value;
		reverb_light = (Boolean) items[12].value;
		load_default_soundbank = (Boolean) items[13].value;
	}

	private String patchToString(Patch patch) {
		if (patch instanceof ModelPatch && ((ModelPatch) patch).isPercussion())
			return "p." + patch.getProgram() + "." + patch.getBank();
		else
			return patch.getProgram() + "." + patch.getBank();
	}

	private void setFormat(AudioFormat format) {
		if (format.getChannels() > 2) {
			throw new IllegalArgumentException("Only mono and stereo audio supported.");
		}
		if (AudioFloatConverter.getConverter(format) == null)
			throw new IllegalArgumentException("Audio format not supported.");
		this.format = format;
	}

	protected void removeReceiver(Receiver recv) {
		boolean perform_close = false;
		synchronized (control_mutex) {
			if (recvslist.remove(recv)) {
				if (implicitOpen && recvslist.isEmpty())
					perform_close = true;
			}
		}
		if (perform_close)
			close();
	}

	protected SoftMainMixer getMainMixer() {
		if (!isOpen())
			return null;
		return mainmixer;
	}

	protected SoftInstrument findInstrument(int program, int bank, int channel) {

		// Add support for GM2 banks 0x78 and 0x79
		// as specified in DLS 2.2 in Section 1.4.6
		// which allows using percussion and melodic instruments
		// on all channels
		if (bank >> 7 == 0x78 || bank >> 7 == 0x79) {
			SoftInstrument current_instrument = inslist.get(program + "." + bank);
			if (current_instrument != null)
				return current_instrument;

			String p_plaf;
			if (bank >> 7 == 0x78)
				p_plaf = "p.";
			else
				p_plaf = "";

			// Instrument not found fallback to MSB:bank, LSB:0
			current_instrument = inslist.get(p_plaf + program + "." + ((bank & 128) << 7));
			if (current_instrument != null)
				return current_instrument;
			// Instrument not found fallback to MSB:0, LSB:bank
			current_instrument = inslist.get(p_plaf + program + "." + (bank & 128));
			if (current_instrument != null)
				return current_instrument;
			// Instrument not found fallback to MSB:0, LSB:0
			current_instrument = inslist.get(p_plaf + program + ".0");
			if (current_instrument != null)
				return current_instrument;
			// Instrument not found fallback to MSB:0, LSB:0, program=0
			current_instrument = inslist.get(p_plaf + program + "0.0");
			if (current_instrument != null)
				return current_instrument;
			return null;
		}

		// Channel 10 uses percussion instruments
		String p_plaf;
		if (channel == 9)
			p_plaf = "p.";
		else
			p_plaf = "";

		SoftInstrument current_instrument = inslist.get(p_plaf + program + "." + bank);
		if (current_instrument != null)
			return current_instrument;
		// Instrument not found fallback to MSB:0, LSB:0
		current_instrument = inslist.get(p_plaf + program + ".0");
		if (current_instrument != null)
			return current_instrument;
		// Instrument not found fallback to MSB:0, LSB:0, program=0
		current_instrument = inslist.get(p_plaf + "0.0");
		if (current_instrument != null)
			return current_instrument;
		return null;
	}

	protected int getVoiceAllocationMode() {
		return voice_allocation_mode;
	}

	protected int getGeneralMidiMode() {
		return gmmode;
	}

	protected void setGeneralMidiMode(int gmmode) {
		this.gmmode = gmmode;
	}

	protected int getDeviceID() {
		return deviceid;
	}

	protected float getControlRate() {
		return controlrate;
	}

	protected SoftVoice[] getVoices() {
		return voices;
	}

	protected SoftTuning getTuning(Patch patch) {
		String t_id = patchToString(patch);
		SoftTuning tuning = tunings.get(t_id);
		if (tuning == null) {
			tuning = new SoftTuning(patch);
			tunings.put(t_id, tuning);
		}
		return tuning;
	}

	public long getLatency() {
		synchronized (control_mutex) {
			return latency;
		}
	}

	public AudioFormat getFormat() {
		synchronized (control_mutex) {
			return format;
		}
	}

	public int getMaxPolyphony() {
		synchronized (control_mutex) {
			return maxpoly;
		}
	}

	public MidiChannel[] getChannels() {

		synchronized (control_mutex) {
			// if (external_channels == null) => the synthesizer is not open,
			// create 16 proxy channels
			// otherwise external_channels has the same length as channels array
			if (external_channels == null) {
				external_channels = new SoftChannelProxy[16];
				for (int i = 0; i < external_channels.length; i++)
					external_channels[i] = new SoftChannelProxy();
			}
			MidiChannel[] ret;
			if (isOpen())
				ret = new MidiChannel[channels.length];
			else
				ret = new MidiChannel[16];
			for (int i = 0; i < ret.length; i++)
				ret[i] = external_channels[i];
			return ret;
		}
	}

	public VoiceStatus[] getVoiceStatus() {
		if (!isOpen()) {
			VoiceStatus[] tempVoiceStatusArray = new VoiceStatus[getMaxPolyphony()];
			for (int i = 0; i < tempVoiceStatusArray.length; i++) {
				VoiceStatus b = new VoiceStatus();
				b.active = false;
				b.bank = 0;
				b.channel = 0;
				b.note = 0;
				b.program = 0;
				b.volume = 0;
				tempVoiceStatusArray[i] = b;
			}
			return tempVoiceStatusArray;
		}

		synchronized (control_mutex) {
			VoiceStatus[] tempVoiceStatusArray = new VoiceStatus[voices.length];
			for (int i = 0; i < voices.length; i++) {
				VoiceStatus a = voices[i];
				VoiceStatus b = new VoiceStatus();
				b.active = a.active;
				b.bank = a.bank;
				b.channel = a.channel;
				b.note = a.note;
				b.program = a.program;
				b.volume = a.volume;
				tempVoiceStatusArray[i] = b;
			}
			return tempVoiceStatusArray;
		}
	}

	public boolean isSoundbankSupported(Soundbank soundbank) {
		return !Stream.of(soundbank.getInstruments()).filter(ins->!(ins instanceof ModelInstrument)).findAny().isPresent();
	}

	public boolean loadInstrument(Instrument instrument) {
		if (instrument == null || (!(instrument instanceof ModelInstrument))) {
			throw new IllegalArgumentException("Unsupported instrument: " + instrument);
		}
		List<ModelInstrument> instruments = new ArrayList<ModelInstrument>();
		instruments.add((ModelInstrument) instrument);
		return loadInstruments(instruments);
	}

	public void unloadInstrument(Instrument instrument) {
		if (instrument == null || (!(instrument instanceof ModelInstrument))) {
			throw new IllegalArgumentException("Unsupported instrument: " + instrument);
		}
		if (!isOpen())
			return;

		String pat = patchToString(instrument.getPatch());
		synchronized (control_mutex) {
			for (SoftChannel c : channels)
				c.current_instrument = null;
			inslist.remove(pat);
			loadedlist.remove(pat);
			for (int i = 0; i < channels.length; i++) {
				channels[i].allSoundOff();
			}
		}
	}

	public boolean remapInstrument(Instrument from, Instrument to) {

		if (from == null)
			throw new NullPointerException();
		if (to == null)
			throw new NullPointerException();
		if (!(from instanceof ModelInstrument)) {
			throw new IllegalArgumentException("Unsupported instrument: " + from.toString());
		}
		if (!(to instanceof ModelInstrument)) {
			throw new IllegalArgumentException("Unsupported instrument: " + to.toString());
		}
		if (!isOpen())
			return false;

		synchronized (control_mutex) {
			if (!loadedlist.containsValue(to))
				throw new IllegalArgumentException("Instrument to is not loaded.");
			unloadInstrument(from);
			ModelMappedInstrument mfrom = new ModelMappedInstrument((ModelInstrument) to, from.getPatch());
			return loadInstrument(mfrom);
		}
	}

	public Soundbank getDefaultSoundbank() {
		synchronized (SoftSynthesizer.class) {
			if (defaultSoundBank != null)
				return defaultSoundBank;
			try {
				/*
				 * Generate emergency soundbank
				 */
				defaultSoundBank = EmergencySoundbank.createSoundbank();
			} catch (Exception e) {
			}
		}
		return defaultSoundBank;
	}

	public Instrument[] getAvailableInstruments() {
		Soundbank defsbk = getDefaultSoundbank();
		if (defsbk == null)
			return new Instrument[0];
		Instrument[] inslist_array = defsbk.getInstruments();
		Arrays.sort(inslist_array, new ModelInstrumentComparator());
		return inslist_array;
	}

	public Instrument[] getLoadedInstruments() {
		if (!isOpen())
			return new Instrument[0];

		synchronized (control_mutex) {
			ModelInstrument[] inslist_array = new ModelInstrument[loadedlist.values().size()];
			loadedlist.values().toArray(inslist_array);
			Arrays.sort(inslist_array, new ModelInstrumentComparator());
			return inslist_array;
		}
	}

	public boolean loadAllInstruments(Soundbank soundbank) {
		List<ModelInstrument> instruments = new ArrayList<ModelInstrument>();
		for (Instrument ins : soundbank.getInstruments()) {
			if (ins == null || !(ins instanceof ModelInstrument)) {
				throw new IllegalArgumentException("Unsupported instrument: " + ins);
			}
			instruments.add((ModelInstrument) ins);
		}
		return loadInstruments(instruments);
	}

	public void unloadAllInstruments(Soundbank soundbank) {
		if (soundbank == null || !isSoundbankSupported(soundbank))
			throw new IllegalArgumentException("Unsupported soundbank: " + soundbank);

		if (!isOpen())
			return;

		for (Instrument ins : soundbank.getInstruments()) {
			if (ins instanceof ModelInstrument) {
				unloadInstrument(ins);
			}
		}
	}

	public boolean loadInstruments(Soundbank soundbank, Patch[] patchList) {
		List<ModelInstrument> instruments = new ArrayList<ModelInstrument>();
		for (Patch patch : patchList) {
			Instrument ins = soundbank.getInstrument(patch);
			if (ins == null || !(ins instanceof ModelInstrument)) {
				throw new IllegalArgumentException("Unsupported instrument: " + ins);
			}
			instruments.add((ModelInstrument) ins);
		}
		return loadInstruments(instruments);
	}

	public void unloadInstruments(Soundbank soundbank, Patch[] patchList) {
		if (soundbank == null || !isSoundbankSupported(soundbank))
			throw new IllegalArgumentException("Unsupported soundbank: " + soundbank);

		if (!isOpen())
			return;

		for (Patch pat : patchList) {
			Instrument ins = soundbank.getInstrument(pat);
			if (ins instanceof ModelInstrument) {
				unloadInstrument(ins);
			}
		}
	}

	public MidiDevice.Info getDeviceInfo() {
		return info;
	}

	private Properties getStoredProperties() {
		return AccessController.doPrivileged(new PrivilegedAction<Properties>() {
			public Properties run() {
				Properties p = new Properties();
				String notePath = "/com/sun/media/sound/softsynthesizer";
				try {
					Preferences prefroot = Preferences.userRoot();
					if (prefroot.nodeExists(notePath)) {
						Preferences prefs = prefroot.node(notePath);
						String[] prefs_keys = prefs.keys();
						for (String prefs_key : prefs_keys) {
							String val = prefs.get(prefs_key, null);
							if (val != null)
								p.setProperty(prefs_key, val);
						}
					}
				} catch (BackingStoreException e) {
				} catch (SecurityException e) {
				}
				return p;
			}
		});
	}

	public AudioSynthesizerPropertyInfo[] getPropertyInfo(Map<String, Object> info) {
		List<AudioSynthesizerPropertyInfo> list = new ArrayList<AudioSynthesizerPropertyInfo>();

		AudioSynthesizerPropertyInfo item;

		// If info != null or synthesizer is closed
		// we return how the synthesizer will be set on next open
		// If info == null and synthesizer is open
		// we return current synthesizer properties.
		boolean o = info == null && open;

		item = new AudioSynthesizerPropertyInfo("interpolation", o ? resamplerType : "linear");
		item.choices = new String[] { "linear", "linear1", "linear2", "cubic", "lanczos", "sinc", "point" };
		item.description = "Interpolation method";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("control rate", o ? controlrate : 147f);
		item.description = "Control rate";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("format", o ? format : new AudioFormat(44100, 16, 2, true, false));
		item.description = "Default audio format";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("latency", o ? latency : 120000L);
		item.description = "Default latency";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("device id", o ? deviceid : 0);
		item.description = "Device ID for SysEx Messages";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("max polyphony", o ? maxpoly : 64);
		item.description = "Maximum polyphony";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("reverb", o ? reverb_on : true);
		item.description = "Turn reverb effect on or off";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("chorus", o ? chorus_on : true);
		item.description = "Turn chorus effect on or off";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("auto gain control", o ? agc_on : true);
		item.description = "Turn auto gain control on or off";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("large mode", o ? largemode : false);
		item.description = "Turn large mode on or off.";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("midi channels", o ? channels.length : 16);
		item.description = "Number of midi channels.";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("jitter correction", o ? jitter_correction : true);
		item.description = "Turn jitter correction on or off.";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("light reverb", o ? reverb_light : true);
		item.description = "Turn light reverb mode on or off";
		list.add(item);

		item = new AudioSynthesizerPropertyInfo("load default soundbank", o ? load_default_soundbank : true);
		item.description = "Enabled/disable loading default soundbank";
		list.add(item);

		AudioSynthesizerPropertyInfo[] items;
		items = list.toArray(new AudioSynthesizerPropertyInfo[list.size()]);

		Properties storedProperties = getStoredProperties();

		for (AudioSynthesizerPropertyInfo item2 : items) {
			Object v = (info == null) ? null : info.get(item2.name);
			v = (v != null) ? v : storedProperties.getProperty(item2.name);
			if (v != null) {
				@SuppressWarnings("rawtypes")
				Class c = (item2.valueClass);
				if (c.isInstance(v))
					item2.value = v;
				else if (v instanceof String) {
					String s = (String) v;
					if (c == Boolean.class) {
						if (s.equalsIgnoreCase("true"))
							item2.value = Boolean.TRUE;
						if (s.equalsIgnoreCase("false"))
							item2.value = Boolean.FALSE;
					} else if (c == AudioFormat.class) {
						int channels = 2;
						boolean signed = true;
						boolean bigendian = false;
						int bits = 16;
						float sampleRate = 44100f;
						try {
							StringTokenizer st = new StringTokenizer(s, ", ");
							String prevToken = "";
							while (st.hasMoreTokens()) {
								String token = st.nextToken().toLowerCase();
								if (token.equals("mono"))
									channels = 1;
								if (token.startsWith("channel"))
									channels = Integer.parseInt(prevToken);
								if (token.contains("unsigned"))
									signed = false;
								if (token.equals("big-endian"))
									bigendian = true;
								if (token.equals("bit"))
									bits = Integer.parseInt(prevToken);
								if (token.equals("hz"))
									sampleRate = Float.parseFloat(prevToken);
								prevToken = token;
							}
							item2.value = new AudioFormat(sampleRate, bits, channels, signed, bigendian);
						} catch (NumberFormatException e) {
						}

					} else
						try {
							if (c == Byte.class)
								item2.value = Byte.valueOf(s);
							else if (c == Short.class)
								item2.value = Short.valueOf(s);
							else if (c == Integer.class)
								item2.value = Integer.valueOf(s);
							else if (c == Long.class)
								item2.value = Long.valueOf(s);
							else if (c == Float.class)
								item2.value = Float.valueOf(s);
							else if (c == Double.class)
								item2.value = Double.valueOf(s);
						} catch (NumberFormatException e) {
						}
				} else if (v instanceof Number) {
					Number n = (Number) v;
					if (c == Byte.class)
						item2.value = Byte.valueOf(n.byteValue());
					if (c == Short.class)
						item2.value = Short.valueOf(n.shortValue());
					if (c == Integer.class)
						item2.value = Integer.valueOf(n.intValue());
					if (c == Long.class)
						item2.value = Long.valueOf(n.longValue());
					if (c == Float.class)
						item2.value = Float.valueOf(n.floatValue());
					if (c == Double.class)
						item2.value = Double.valueOf(n.doubleValue());
				}
			}
		}

		return items;
	}

	public void open() throws MidiUnavailableException {
		if (isOpen()) {
			synchronized (control_mutex) {
				implicitOpen = false;
			}
			return;
		}
		open(null, null);
	}

	public void open(SourceDataLine line, Map<String, Object> info) throws MidiUnavailableException {
		throw new java.lang.UnsupportedOperationException("Not Implemented Yet!");
	}

	public AudioInputStream openStream(AudioFormat targetFormat, Map<String, Object> info) throws MidiUnavailableException {
		throw new java.lang.UnsupportedOperationException("Not Implemented Yet!");
	}

	public FloatSampleInput openFloatInput(AudioFormat targetFormat, Map<String, Object> info) throws MidiUnavailableException {
		if (isOpen())
			throw new MidiUnavailableException("Synthesizer is already open");

		synchronized (control_mutex) {

			gmmode = 0;
			voice_allocation_mode = 0;

			processPropertyInfo(info);

			open = true;
			implicitOpen = false;

			if (targetFormat != null)
				setFormat(targetFormat);

			if (load_default_soundbank) {
				Soundbank defbank = getDefaultSoundbank();
				if (defbank != null) {
					loadAllInstruments(defbank);
				}
			}

			voices = new SoftVoice[maxpoly];
			for (int i = 0; i < maxpoly; i++)
				voices[i] = new SoftVoice(this);

			mainmixer = new SoftMainMixer(this);

			channels = new SoftChannel[number_of_midi_channels];
			for (int i = 0; i < channels.length; i++)
				channels[i] = new SoftChannel(this, i);

			if (external_channels == null) {
				// Always create external_channels array with 16 or more channels so getChannels works correctly when the synhtesizer is closed.
				if (channels.length < 16)
					external_channels = new SoftChannelProxy[16];
				else
					external_channels = new SoftChannelProxy[channels.length];
				for (int i = 0; i < external_channels.length; i++)
					external_channels[i] = new SoftChannelProxy();
			} else {
				// We must resize external_channels array but we must also copy the old SoftChannelProxy into the new one
				if (channels.length > external_channels.length) {
					SoftChannelProxy[] new_external_channels = new SoftChannelProxy[channels.length];
					for (int i = 0; i < external_channels.length; i++)
						new_external_channels[i] = external_channels[i];
					for (int i = external_channels.length; i < new_external_channels.length; i++) {
						new_external_channels[i] = new SoftChannelProxy();
					}
				}
			}

			for (int i = 0; i < channels.length; i++)
				external_channels[i].setChannel(channels[i]);

			for (SoftVoice voice : getVoices())
				voice.resampler = resampler.openStreamer();

			for (Receiver recv : getReceivers()) {
				SoftReceiver srecv = ((SoftReceiver) recv);
				srecv.open = open;
				srecv.mainmixer = mainmixer;
				srecv.midimessages = mainmixer.midimessages;
			}
		}
		return mainmixer;
	}

	public void close() {

		if (!isOpen())
			return;

		synchronized (control_mutex) {

			if (mainmixer != null)
				mainmixer.close();
			open = false;
			implicitOpen = false;
			mainmixer = null;
			voices = null;
			channels = null;

			if (external_channels != null)
				for (int i = 0; i < external_channels.length; i++)
					external_channels[i].setChannel(null);

			inslist.clear();
			loadedlist.clear();
			tunings.clear();

			while (recvslist.size() != 0)
				recvslist.get(recvslist.size() - 1).close();

		}
	}

	public boolean isOpen() {
		synchronized (control_mutex) {
			return open;
		}
	}

	public long getMicrosecondPosition() {
		if (!isOpen())
			return 0;
		synchronized (control_mutex) {
			if(mainmixer==null){
				return 0;
			}
			return mainmixer.getMicrosecondPosition();
		}
	}

	public int getMaxReceivers() {
		return -1;
	}

	public int getMaxTransmitters() {
		return 0;
	}

	public Receiver getReceiver() throws MidiUnavailableException {
		synchronized (control_mutex) {
			SoftReceiver receiver = new SoftReceiver(this);
			receiver.open = open;
			recvslist.add(receiver);
			return receiver;
		}
	}

	public List<Receiver> getReceivers() {
		synchronized (control_mutex) {
			return Collections.unmodifiableList(recvslist);
		}
	}

	public Transmitter getTransmitter() throws MidiUnavailableException {
		throw new MidiUnavailableException("No transmitter available");
	}

	public List<Transmitter> getTransmitters() {
		return Collections.emptyList();
	}

	public Receiver getReceiverReferenceCounting() throws MidiUnavailableException {
		if (!isOpen()) {
			open();
			synchronized (control_mutex) {
				implicitOpen = true;
			}
		}
		return getReceiver();
	}

	public Transmitter getTransmitterReferenceCounting() throws MidiUnavailableException {
		throw new MidiUnavailableException("No transmitter available");
	}
}
